#-*- coding: utf-8 -*-
"""
TP 7 - Traitement d'images (version améliorée)
Par Guillaume HUYSMANS et Mathieu LECLERQ, 2013.

Cet outil est capable d'appliquer différents filtres à une image au format JPEG.
Il fonctionne en ligne de commande et peut donc être utilisé depuis un batch 
ou un autre logiciel. [Vu sa lenteur "naturelle", j'ai des doutes, mais bon.]

$$$app$$$ inp out ftr1 ftr2... [/o]
/o permet d'ouvrir directement l'image produite.
$$$filters$$$
"""
"""
Dépendances (pas très intéressantes à mettre dans l'aide) :
	unidecode (https://pypi.python.org/pypi/Unidecode)
		Pour l'installer, décompressez l'archive et tapez 
		ces deux commandes :
		$ python setup.py install
		$ python setup.py test
		Sous Windows, la console doit être ouverte 
		"en tant qu'administrateur" pour que ça fonctionne.
	Pillow (builds non officiels, mais dispos pour Win64 : 
	http://www.lfd.uci.edu/~gohlke/pythonlibs/#pillow)
		Pour l'installer, sélectionnez la version 
		correspondant à votre installation Python
		et téléchargez/lancez l'assistant d'installation...
"""



from umage import load, save #module utilisé en cours d'algo en BA1 à l'UMONS
from copy import deepcopy #pour copier des listes de façon récursive
#from math import radians, sin, cos #pour tracer un cercle...
from unidecode import unidecode #pour que les accents s'affichent correctement sous Windows
import sys #pour gérer les arguments issus de la ligne de commande


class ClError(Exception):
	"""Erreur de parsing de la ligne de commande."""
	pass

def greyscale(img):
	"""Convertit l'image en nuances de gris."""
	cp = deepcopy(img)
	for y in range(len(cp)):
		for x in range(len(cp[0])):
			pix = cp[y][x]
			nivgris = int(0.2125*pix[0] + 0.7154*pix[1] + 0.0721*pix[2])
			cp[y][x] = (nivgris, nivgris, nivgris)
	return cp

def invert(img):
	"""Retourne un "négatif" de l'image."""
	cp = deepcopy(img)
	for y in range(len(cp)):
		for x in range(len(cp[0])):
			pix = cp[y][x]
			cp[y][x] = (255-pix[0], 255-pix[1], 255-pix[2])
	return cp

def convo(img, mat):
	"""Applique une matrice de convolution à toute l'image."""
	#On ajoute un bord noir partout. Cette matrice sera celle à partir
	#de laquelle on fera les calculs avec la matrice de convolution mat.
	orig = deepcopy(img)
	for i in range(len(orig)):
		orig[i] = [(0, 0, 0)] + orig[i] + [(0, 0, 0)]
	vide = [(0, 0, 0) for useless in range(len(orig[0]))]
	orig.insert(0, vide) #première ligne
	orig.append(vide)    #dernière ligne
	#On crée une matrice vide contenant le résultat
	res = [[(0, 0, 0)] * len(orig[0]) for useless in range(len(orig))]
	#Pour chaque vrai pixel, on calcule avec la matrice de convolution.
	for y in range(1,len(orig)-1):
		for x in range(1,len(orig[0])-1):
			#Produit de convolution (avec les voisins)
			accu = 0
			for i in range(len(mat)):
				for j in range(len(mat[0])):
					accu += orig[y-1+i][x-1+j][0] * mat[i][j]
			#On borne le résultat entre 0 et 255
			if accu>255:
				accu = 255
			elif accu<0:
				accu = 0
			#On stocke le résultat
			res[y][x] = (accu, accu, accu)
	#On retire les bords noirs
	for i in range(len(res)):
		res[i] = res[i][1:-1]
	res.pop()  #dernière ligne
	res.pop(0) #première ligne
	return res

#Ne pas insérer des éléments, ça pourrait faire déconner les dépendances.
#En ajouter, par contre, ne peut pas faire de tort.
filters = [	(u'contours (tolérant)', [[1, 0, -1], [2, 0, -2], [1, 0, -1]]), 
			(u'contours (normal)', [[-1,-1,-1],[-1,8,-1],[-1,-1,-1]]), 
			(u'croquis', [[-1,-1,-1],[-1,9,-1],[-1,-1,-1]]), 
			(u'relief', [[-2,0,0],[0,1,0],[0,0,2]]),
			(u'négatif', invert),
			(u'nuances de gris', greyscale)]
	
def is_greyscale(img):
	"""Retourne True si l'image en paramètre est déjà en nuances de gris."""
	for ligne in img:
		for pt in ligne:
			if pt[0]!=pt[1] or pt[1]!=pt[2] or pt[0]!=pt[2]:
				return False
	return True

def circle(img, pt, radius, color):
	"""
	Dessine un cercle sur img. Ça ne donne pas très bien quand le JPEG est trop compressé...
	Non utilisée, cette fonction est juste là pour l'exercice supplémentaire.
	"""
	res = deepcopy(img)
	for angle in range(0, 361): #FIXME les points ne sont même pas joints...
		x = int(pt[0] + cos(radians(angle)) * radius)
		y = int(pt[1] + sin(radians(angle)) * radius)
		if (x in range(0, len(img[0]))) and (y in range(0, len(img))):
			res[y][x] = color
	return res

def shellExecute(f):
	"""
	Ouvre un fichier dont le nom est passé en paramètre.
	Testé sous Windows et Ubuntu (testé)...
	Pris depuis http://stackoverflow.com/a/435669
	"""
	import subprocess, os, sys
	if sys.platform.startswith('darwin'):
		subprocess.call(('open', f))
	elif os.name == 'nt':
		os.startfile(f)
	elif os.name == 'posix':
		subprocess.call(('xdg-open', f))

def getFiltersList():
	"""Retourne une liste formatée des filtres disponibles."""
	s = ""
	for i, c in enumerate(filters):
		s += u">>> Filtre %d : %s\n" % (i+1, c[0])
	return s

def convertArguments():
	"""Convertit les paramètres de la ligne de commande (pour gérer les accents)."""
	if sys.platform == 'win32':
		#les caractères spécifiques au français (éèêç, etc.) seront mal affichés
		#si on ne force pas French characters won't work if we don't force the input encoding
		enc = 'iso-8859-1'
	else:
		enc = sys.stdin.encoding
	args = []
	for a in sys.argv:
		args.append(unicode(a, enc))
	sys.argv = args

def runFilter(img, id):
	"""Applique le filtre #id à l'image (une copie est retournée)."""
	if isinstance(filters[id][1], list):
		if not is_greyscale(img):
			print u"Dépendance :",
			tmp = runFilter(img, 5) #nuances de gris
		else:
			tmp = img #un simple alias
		print "Application du filtre '%s'..." % filters[id][0]
		tmp = convo(tmp, filters[id][1])
	else:
		print "Application du filtre '%s'..." % filters[id][0]
		tmp = filters[id][1](img)
	return tmp



if len(sys.argv) < 4:
	#Pas assez de paramètres, on affiche l'aide et les crédits.
	#On la lit depuis le commentaire en tête de fichier...
	help = unicode(__doc__.strip(), "utf-8")
	help = help.replace("$$$filters$$$", getFiltersList())
	help = help.replace("$$$app$$$", sys.argv[0])
	print help,
else:
	convertArguments()
	try:
		#Parsing (simple) du nom de fichier de sortie
		out_full = sys.argv[2]
		pos = out_full.rfind('.')
		if pos == -1:
			raise ClError(u"Extension non trouvée pour le fichier de sortie !")
		out = out_full[:pos]		#nom sans l'extension
		out_fmt = out_full[pos+1:]	#extension sans le point
		#Chargement du fichier d'entrée
		print "Chargement..."
		img = load(sys.argv[1])
		#Parsing (un peu moins simple) des paramètres
		shell = False
		for f in sys.argv[3:]: #à partir du premier filtre
			if f.isnumeric() and (int(f) in range(1, len(filters)+1)):
				f = int(f)-1 #on en fait un indice
				img = runFilter(img, f)
			elif f.lower()=='/o':
				shell = True
			else:
				raise ClError("Filtre inconnu : '%s'." % f)
		print "Enregistrement..."
		save(img, out, out_fmt) #FIXME gérer exception format inconnu
		print u"Terminé."
		if shell:
			shellExecute(out_full)
	except Exception as e:
		print "!!! Une erreur est survenue."
		print str(e)